﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace PubSync
{
    class Program
    {
        static string RobocopyArguments;
        static bool _showCommands = false;
        static bool _runCommands = true;
        private static bool _copyFiles = true;
        private static bool _noDelete = false;

        static readonly ProcessStartInfo RobocopyInfo = new ProcessStartInfo
        {
            FileName = "robocopy.exe",
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            CreateNoWindow = true,
            UseShellExecute = false
        };

        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Publishing profile not specified!");
                return;
            }
            RobocopyArguments = "{0} {1}\\{0} /NJH /NP /NDL /S ";
            if (args.Contains("--noDelete"))
            {
                _noDelete = true;
                RobocopyArguments += " /MIR ";
            }
            if (args.Contains("--showCommands"))
                _showCommands = true;
            if (args.Contains("--noCopy"))
                _copyFiles = false;
            if (args.Contains("--noRun"))
                _runCommands = false;
            RobocopyArguments += " {2} {3} {4}";

            var profileName = args[0];

            SyncFolders(profileName, LoadAndValidateXml());
            Console.WriteLine("Sync finished.");
        }

        static void GetFilesInDir(int basePathLength, DirectoryInfo dir, List<Regex> excludedDirectories, List<Regex> excludedFiles, Dictionary<string, FileInfo> filesDictionary)
        {
            foreach (var file in dir.GetFiles().Where(f => excludedFiles.All(e => !e.IsMatch(f.Name))))
                filesDictionary.Add(file.FullName.Substring(basePathLength + 1), file);
            foreach (var subDir in dir.GetDirectories().Where(subDir => excludedDirectories.All(e => !e.IsMatch(subDir.Name))))
                GetFilesInDir(basePathLength, subDir, excludedDirectories, excludedFiles, filesDictionary);
        }

        static XmlSchemaSet LoadSchema()
        {
            var schemas = new XmlSchemaSet();
            var pubsyncXsdFile = new FileInfo(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, "pubsync.xsd"));
            if (!pubsyncXsdFile.Exists)
            {
                Console.WriteLine("Unable to locate pubsync.xsd!\r\nPath: {0}", pubsyncXsdFile.FullName);
                return null;
            }
            bool errors = false;

            var xmlSchema = XmlSchema.Read(XmlReader.Create(pubsyncXsdFile.FullName), (o, error) =>
            {
                Console.WriteLine(error.Message);
                errors = true;
            });
            if (errors)
            {
                Console.WriteLine("Error parsing pubsync.xsd!\r\nPath: {0}", pubsyncXsdFile.FullName);
                return null;
            }
            schemas.Add(xmlSchema);

            return schemas;
        }

        static XDocument LoadAndValidateXml()
        {
            bool errors = false;
            var pubsyncXmlFile = new FileInfo("pubsync.xml");
            if (!pubsyncXmlFile.Exists)
            {
                Console.WriteLine("Unable to locate pubsync.xml!\r\nPath: {0}", pubsyncXmlFile.FullName);
                return null;
            }

            var schemas = LoadSchema();
            if (schemas == null)
                return null;

            var xdoc = XDocument.Load("pubsync.xml");

            xdoc.Validate(schemas, (o, error) =>
            {
                Console.WriteLine("{0}", error.Message);
                errors = true;
            });

            if (errors)
            {
                Console.WriteLine("Pubsync.xml does not adhere to the schema!\r\nPath: {0}", pubsyncXmlFile.FullName);
                return null;
            }
            return xdoc;
        }

        static bool SyncFolders(string profileName, XDocument xdoc)
        {
            if (xdoc == null)
                return false;

            var root = xdoc.Root;
            var ns = "{" + root.Name.Namespace + "}";
            var profile = root.Element(ns + "Profiles").Elements(ns + "Profile").SingleOrDefault(d => d.Name == ns + "Profile" && d.Attribute("Name").Value == profileName);
            if (profile == null)
            {
                Console.WriteLine("Profile '{0}' does not exist!", profileName);
                return false;
            }
            var publishingPath = profile.Attribute("PublishingPath").Value;

            // Copy the files!
            foreach (var folder in root.Element(ns + "Folders").Elements(ns + "Folder"))
            {
                var copyMethod = folder.Attribute("CopyMethod");
                if (copyMethod != null && copyMethod.Value.ToLower() == "pubsync")
                    SyncUsingPubsync(publishingPath, ns, folder);
                else
                    SyncUsingRobocopy(publishingPath, ns, folder);
            }

            return true;
        }

        static void SyncUsingPubsync(string publishingPath, string xmlNamespace, XElement folder)
        {
            try
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();

                var folderPath = folder.Attribute("Path").Value;
                Console.WriteLine("{0}", folderPath);
                Console.WriteLine("------------------------------------------------------------------------------");

                var destDir = new DirectoryInfo(Path.Combine(publishingPath, folderPath));
                var destFilesDictionary = new Dictionary<string, FileInfo>();

                var filesToExclude = folder.Elements(xmlNamespace + "Exclude").Where(e => e.Attribute("Type").Value == "File")
                   .Select(
                             d =>
                             new Regex(d.Attribute("Expression").Value, RegexOptions.Compiled & RegexOptions.IgnoreCase))
                         .ToList();


                var foldersToExclude =
                folder.Elements(xmlNamespace + "Exclude").Where(e => e.Attribute("Type").Value == "Folder")
                      .Select(
                          d =>
                          new Regex(d.Attribute("Expression").Value, RegexOptions.Compiled & RegexOptions.IgnoreCase))
                      .ToList();

                var sourceDir = new DirectoryInfo(folderPath);
                var sourceFilesDictionary = new Dictionary<string, FileInfo>();

                GetFilesInDir(sourceDir.FullName.Length, sourceDir, foldersToExclude, filesToExclude, sourceFilesDictionary);
                GetFilesInDir(destDir.FullName.Length, destDir, foldersToExclude, filesToExclude, destFilesDictionary);

                int changedFilesCount = 0;
                int deletedFilesCount = 0;
                int newFilesCount = 0;
                int skippedFilesCount = 0;
                int errorCount = 0;

                // Check source files for new / changed
                foreach (var sourceFileEntry in sourceFilesDictionary)
                {
                    try
                    {
                        FileInfo destFile;

                        if (destFilesDictionary.TryGetValue(sourceFileEntry.Key, out destFile))
                        {
                            if (sourceFileEntry.Value.Length != destFile.Length ||
                                sourceFileEntry.Value.LastWriteTime != destFile.LastWriteTime)
                            {
                                // Console.WriteLine("C: {0}", sourceFileEntry.Value.Name);
                                if (_runCommands)
                                    sourceFileEntry.Value.CopyTo(Path.Combine(destDir.FullName, sourceFileEntry.Key), true);
                                changedFilesCount++;
                            }
                            else
                                skippedFilesCount++;
                        }
                        else
                        {
                            // Console.WriteLine("N: {0}", sourceFileEntry.Value.Name);
                            if (_copyFiles)
                            {
                                destFile = new FileInfo(Path.Combine(destDir.FullName, sourceFileEntry.Key));
                                if (!destFile.Directory.Exists)
                                    destFile.Directory.Create();
                                sourceFileEntry.Value.CopyTo(destFile.FullName);
                            }
                            newFilesCount++;
                        }
                    }
                    catch (Exception ex)
                    {
                        errorCount++;
                    }
                }

                // Check for orphaned files to delete
                if (!_noDelete)
                    foreach (var fileKey in destFilesDictionary.Keys.Except(sourceFilesDictionary.Keys))
                    {
                        try
                        {
                            destFilesDictionary[fileKey].Delete();
                            deletedFilesCount++;
                        }
                        catch (Exception e)
                        {
                            errorCount++;
                        }
                    }

                stopwatch.Stop();
                Console.WriteLine("Skipped: {0}\r\nNew: {1}\r\nChanged: {2}\r\nDeleted: {3}\r\nErrors: {4}", skippedFilesCount, newFilesCount, changedFilesCount, deletedFilesCount, errorCount);
                Console.WriteLine("Time: {0}", stopwatch.Elapsed);
                Console.WriteLine("==============================================================================");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void SyncUsingRobocopy(string publishingPath, string xmlNamespace, XElement folder)
        {
            var stopWatch = new Stopwatch();
            var foldersToExclude = folder.Elements(xmlNamespace + "Exclude").Where(e => e.Attribute("Type").Value == "Folder").Select(d => d.Attribute("Expression").Value).ToList();
            var filesToExclude = folder.Elements(xmlNamespace + "Exclude").Where(e => e.Attribute("Type").Value == "File").Select(d => d.Attribute("Expression").Value).ToList();
            var robocopy = new Process { StartInfo = RobocopyInfo };
            var folderPath = folder.Attribute("Path").Value;
            var levelsToSync = folder.Attribute("LevelsToSync");

            robocopy.StartInfo.Arguments = String.Format(RobocopyArguments,
                                                         new object[]
                                                                 {
                                                                     folderPath,
                                                                     publishingPath,
                                                                     foldersToExclude.Any() ? "/XD " + String.Join(" ", foldersToExclude) : "",
                                                                     filesToExclude.Any() ? "/XF " + String.Join(" ", filesToExclude) : "",
                                                                     levelsToSync != null ? "/LEV:" + levelsToSync.Value : ""
                                                                 });
            if (_showCommands)
                Console.WriteLine("robocopy {0}", robocopy.StartInfo.Arguments);
            Console.WriteLine("{0}", folderPath);
            var robocopyOutput = new StringBuilder();
            robocopyOutput.AppendLine("------------------------------------------------------------------------------");
            if (_runCommands)
            {
                robocopy.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data != null)
                        robocopyOutput.AppendLine(e.Data);
                };
                robocopy.EnableRaisingEvents = true;
                stopWatch.Start();
                robocopy.Start();
                robocopy.BeginOutputReadLine();
                robocopy.WaitForExit();
            }
            stopWatch.Stop();
            robocopyOutput.AppendLine(String.Format("Time: {0}", stopWatch.Elapsed));
            robocopyOutput.AppendLine("==============================================================================");

            var outputString = Regex.Replace(robocopyOutput.ToString(), @"\r\n\r\n---+\r\n", "");
            outputString = Regex.Replace(outputString, @"\r\n\s+Times :.*?Ended :.*?\r\n", "\r\n", RegexOptions.Singleline);

            Console.WriteLine(outputString);
        }

    }
}
